/* Copyright 2012 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/** @typedef {import("./interfaces.js").IL10n} IL10n */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentLoadingTask} PDFDocumentLoadingTask */

import {
    animationStarted,
    apiPageLayoutToViewerModes,
    apiPageModeToSidebarView,
    AutoPrintRegExp,
    CursorTool,
    DEFAULT_SCALE_VALUE,
    getActiveOrFocusedElement,
    isValidRotation,
    isValidScrollMode,
    isValidSpreadMode,
    normalizeWheelEventDirection,
    parseQueryString,
    ProgressBar,
    RenderingStates,
    ScrollMode,
    SidebarView,
    SpreadMode,
    TextLayerMode,
} from "./ui_utils.js";
import {
    AnnotationEditorType,
    build,
    FeatureTest,
    getDocument,
    getFilenameFromUrl,
    getPdfFilenameFromUrl,
    GlobalWorkerOptions,
    InvalidPDFException,
    isDataScheme,
    isPdfFile,
    MissingPDFException,
    PDFWorker,
    shadow,
    UnexpectedResponseException,
    version,
} from "./pdfjs";
import {AppOptions, OptionKind} from "./app_options.js";
import {EventBus, FirefoxEventBus} from "./event_utils.js";
import {ExternalServices, initCom, MLManager} from "./genericcom.js";
import {
    ImageAltTextSettings,
    NewAltTextManager,
} from "./new_alt_text_manager";
import {LinkTarget, PDFLinkService} from "./pdf_link_service.js";
import {AnnotationEditorParams} from "./annotation_editor_params";
import {CaretBrowsingMode} from "./caret_browsing.js";
import {DownloadManager} from "./download_manager";
import {OverlayManager} from "./overlay_manager.js";
import {PasswordPrompt} from "./password_prompt.js";
import {PDFAttachmentViewer} from "./pdf_attachment_viewer";
import {PDFCursorTools} from "./pdf_cursor_tools";
import {PDFDocumentProperties} from "./pdf_document_properties";
import {PDFFindBar} from "./pdf_find_bar";
import {PDFFindController} from "./pdf_find_controller.js";
import {PDFHistory} from "./pdf_history.js";
import {PDFLayerViewer} from "./pdf_layer_viewer";
import {PDFOutlineViewer} from "./pdf_outline_viewer";
import {PDFPresentationMode} from "./pdf_presentation_mode";
import {PDFPrintServiceFactory} from "./pdf_print_service.js";
import {PDFRenderingQueue} from "./pdf_rendering_queue.js";
import {PDFScriptingManager} from "./pdf_scripting_manager.js";
import {PDFSidebar} from "./pdf_sidebar";
import {PDFThumbnailViewer} from "./pdf_thumbnail_viewer";
import {PDFViewer} from "./pdf_viewer.js";
import {Preferences} from "./genericcom.js";
import {SecondaryToolbar} from "./secondary_toolbar";
import {Toolbar} from "./toolbar";
import {ViewHistory} from "./view_history.js";
import {hasClosestByClassName} from "../../protyle/util/hasClosest";
import {Constants} from "../../constants";
import {getPdfInstance, hlPDFRect} from "../anno";

const FORCE_PAGES_LOADED_TIMEOUT = 10000; // ms

const ViewOnLoad = {
    UNKNOWN: -1,
    PREVIOUS: 0, // Default value.
    INITIAL: 1,
};

// NOTE
class PDFViewerApplication {
    constructor(pdfId) {
        this.pdfId = pdfId
        this.initialBookmark = document.location.hash.substring(1);
        this._initializedCapability = {
            ...Promise.withResolvers(),
            settled: false,
        }
        this.appConfig = null
        /** @type {PDFDocumentProxy} */
        this.pdfDocument = null
        /** @type {PDFDocumentLoadingTask} */
        this.pdfLoadingTask = null
        this.printService = null
        /** @type {PDFViewer} */
        this.pdfViewer = null
        /** @type {PDFThumbnailViewer} */
        this.pdfThumbnailViewer = null
        /** @type {PDFRenderingQueue} */
        this.pdfRenderingQueue = null
        /** @type {PDFPresentationMode} */
        this.pdfPresentationMode = null
        /** @type {PDFDocumentProperties} */
        this.pdfDocumentProperties = null
        /** @type {PDFLinkService} */
        this.pdfLinkService = null
        /** @type {PDFHistory} */
        this.pdfHistory = null
        /** @type {PDFSidebar} */
        this.pdfSidebar = null
        /** @type {PDFOutlineViewer} */
        this.pdfOutlineViewer = null
        /** @type {PDFAttachmentViewer} */
        this.pdfAttachmentViewer = null
        /** @type {PDFLayerViewer} */
        this.pdfLayerViewer = null
        /** @type {PDFCursorTools} */
        this.pdfCursorTools = null
        /** @type {PDFScriptingManager} */
        this.pdfScriptingManager = null
        /** @type {ViewHistory} */
        this.store = null
        /** @type {DownloadManager} */
        this.downloadManager = null
        /** @type {OverlayManager} */
        this.overlayManager = null
        /** @type {Preferences} */
        this.preferences = new Preferences()
        /** @type {Toolbar} */
        this.toolbar = null
        /** @type {SecondaryToolbar} */
        this.secondaryToolbar = null
        /** @type {EventBus} */
        this.eventBus = null
        /** @type {IL10n} */
        this.l10n = null
        /** @type {AnnotationEditorParams} */
        this.annotationEditorParams = null
        /** @type {ImageAltTextSettings} */
        this.imageAltTextSettings = null
        this.isInitialViewSet = false
        // NOTE 不使用 initialBookmark
        this.isViewerEmbedded = true
        this.url = ""
        this.baseUrl = ""
        this.mlManager = null
        this._downloadUrl = ""
        this._eventBusAbortController = null
        this._windowAbortController = null
        this._globalAbortController = new AbortController()
        this.documentInfo = null
        this.metadata = null
        this._contentDispositionFilename = null
        this._contentLength = null
        this._saveInProgress = false
        this._wheelUnusedTicks = 0
        this._wheelUnusedFactor = 1
        this._touchUnusedTicks = 0
        this._touchUnusedFactor = 1
        this._PDFBug = null
        this._hasAnnotationEditors = false
        this._title = document.title
        this._printAnnotationStoragePromise = null
        this._touchInfo = null
        this._isCtrlKeyDown = false
        this._caretBrowsing = null
        this._isScrolling = false
    }

    // Called once when the document is loaded.
    async initialize(appConfig) {
        this.appConfig = appConfig;

        // Ensure that `Preferences`, and indirectly `AppOptions`, have initialized
        // before creating e.g. the various viewer components.
        try {
            await this.preferences.initializedPromise;
        } catch (ex) {
            console.error(`initialize: "${ex.message}".`);
        }
        if (AppOptions.get("pdfBugEnabled")) {
            await this._parseHashParams();
        }

        if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
            let mode;
            switch (AppOptions.get("viewerCssTheme")) {
                case 1:
                    mode = "is-light";
                    break;
                case 2:
                    mode = "is-dark";
                    break;
            }
            if (mode) {
                document.documentElement.classList.add(mode);
            }
            if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
                if (AppOptions.get("enableFakeMLManager")) {
                    this.mlManager =
                        MLManager.getFakeMLManager?.({
                            enableGuessAltText: AppOptions.get("enableGuessAltText"),
                            enableAltTextModelDownload: AppOptions.get(
                                "enableAltTextModelDownload"
                            ),
                        }) || null;
                }
            }
        } else if (AppOptions.get("enableAltText")) {
            // We want to load the image-to-text AI engine as soon as possible.
            this.mlManager = new MLManager({
                enableGuessAltText: AppOptions.get("enableGuessAltText"),
                enableAltTextModelDownload: AppOptions.get(
                    "enableAltTextModelDownload"
                ),
                altTextLearnMoreUrl: AppOptions.get("altTextLearnMoreUrl"),
            });
        }

        // Ensure that the `L10n`-instance has been initialized before creating
        // e.g. the various viewer components.
        this.l10n = await this.externalServices.createL10n();
        document.getElementsByTagName("html")[0].dir = this.l10n.getDirection();
        // Connect Fluent, when necessary, and translate what we already have.
        if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
            this.l10n.translate(appConfig.appContainer || document.documentElement);
        }

        if (
            this.isViewerEmbedded &&
            AppOptions.get("externalLinkTarget") === LinkTarget.NONE
        ) {
            // Prevent external links from "replacing" the viewer,
            // when it's embedded in e.g. an <iframe> or an <object>.
            AppOptions.set("externalLinkTarget", LinkTarget.TOP);
        }
        await this._initializeViewerComponents();

        // Bind the various event handlers *after* the viewer has been
        // initialized, to prevent errors if an event arrives too soon.
        this.bindEvents();
        this.bindWindowEvents();

        this._initializedCapability.settled = true;
        this._initializedCapability.resolve();
    }

    /**
     * Potentially parse special debugging flags in the hash section of the URL.
     * @private
     */
    async _parseHashParams() {
        const hash = document.location.hash.substring(1);
        if (!hash) {
            return;
        }
        const {mainContainer, viewerContainer} = this.appConfig,
            params = parseQueryString(hash);

        const loadPDFBug = async () => {
            if (this._PDFBug) {
                return;
            }

            // NOTE
            // const {PDFBug} =
            //     typeof PDFJSDev === "undefined"
            //         ? await import(AppOptions.get("debuggerSrc")) // eslint-disable-line no-unsanitized/method
            //         : await __non_webpack_import__(AppOptions.get("debuggerSrc"));
            //
            // this._PDFBug = PDFBug;
        };

        // Parameters that need to be handled manually.
        if (params.get("disableworker") === "true") {
            try {
                GlobalWorkerOptions.workerSrc ||= AppOptions.get("workerSrc");

                if (typeof PDFJSDev === "undefined") {
                    // NOTE
                    globalThis.pdfjsWorker = await import(`${Constants.PROTYLE_CDN}/js/pdf/pdf.worker.mjs?v=4.7.85`);
                } else {
                    await __non_webpack_import__(PDFWorker.workerSrc);
                }
            } catch (ex) {
                console.error(`_parseHashParams: "${ex.message}".`);
            }
        }
        if (params.has("textlayer")) {
            switch (params.get("textlayer")) {
                case "off":
                    AppOptions.set("textLayerMode", TextLayerMode.DISABLE);
                    break;
                case "visible":
                case "shadow":
                case "hover":
                    viewerContainer.classList.add(`textLayer-${params.get("textlayer")}`);
                    try {
                        await loadPDFBug();
                        this._PDFBug.loadCSS();
                    } catch (ex) {
                        console.error(`_parseHashParams: "${ex.message}".`);
                    }
                    break;
            }
        }
        if (params.has("pdfbug")) {
            AppOptions.setAll({pdfBug: true, fontExtraProperties: true});

            const enabled = params.get("pdfbug").split(",");
            try {
                await loadPDFBug();
                this._PDFBug.init(mainContainer, enabled);
            } catch (ex) {
                console.error(`_parseHashParams: "${ex.message}".`);
            }
        }
        // It is not possible to change locale for the (various) extension builds.
        if (
            (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
            params.has("locale")
        ) {
            AppOptions.set("localeProperties", {lang: params.get("locale")});
        }

        // Parameters that can be handled automatically.
        const opts = {
            disableAutoFetch: x => x === "true",
            disableFontFace: x => x === "true",
            disableHistory: x => x === "true",
            disableRange: x => x === "true",
            disableStream: x => x === "true",
            verbosity: x => x | 0,
        };

        // Set some specific preferences for tests.
        if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
            Object.assign(opts, {
                enableAltText: x => x === "true",
                enableFakeMLManager: x => x === "true",
                enableGuessAltText: x => x === "true",
                enableUpdatedAddImage: x => x === "true",
                highlightEditorColors: x => x,
                maxCanvasPixels: x => parseInt(x),
                spreadModeOnLoad: x => parseInt(x),
                supportsCaretBrowsingMode: x => x === "true",
            });
        }

        for (const name in opts) {
            const check = opts[name],
                key = name.toLowerCase();

            if (params.has(key)) {
                AppOptions.set(name, check(params.get(key)));
            }
        }
    }

    /**
     * @private
     */
    async _initializeViewerComponents() {
        const {appConfig, externalServices, l10n} = this;

        const eventBus =
            typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
                ? new FirefoxEventBus(
                    AppOptions.get("allowedGlobalEvents"),
                    externalServices,
                    AppOptions.get("isInAutomation")
                )
                : new EventBus();
        this.eventBus = AppOptions.eventBus = eventBus;
        this.mlManager?.setEventBus(eventBus, this._globalAbortController.signal);

        this.overlayManager = new OverlayManager();

        const pdfRenderingQueue = new PDFRenderingQueue();
        pdfRenderingQueue.onIdle = this._cleanup.bind(this);
        this.pdfRenderingQueue = pdfRenderingQueue;

        const pdfLinkService = new PDFLinkService({
            eventBus,
            externalLinkTarget: AppOptions.get("externalLinkTarget"),
            externalLinkRel: AppOptions.get("externalLinkRel"),
            ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"),
        });
        this.pdfLinkService = pdfLinkService;

        const downloadManager = (this.downloadManager = new DownloadManager());

        const findController = new PDFFindController({
            linkService: pdfLinkService,
            eventBus,
            updateMatchesCountOnProgress:
                typeof PDFJSDev === "undefined"
                    ? !window.isGECKOVIEW
                    : !PDFJSDev.test("GECKOVIEW"),
        });
        this.findController = findController;

        const pdfScriptingManager = new PDFScriptingManager({
            eventBus,
            externalServices,
            docProperties: this._scriptingDocProperties.bind(this),
        });
        this.pdfScriptingManager = pdfScriptingManager;

        const container = appConfig.mainContainer,
            viewer = appConfig.viewerContainer;
        const annotationEditorMode = AppOptions.get("annotationEditorMode");
        const pageColors =
            AppOptions.get("forcePageColors") ||
            window.matchMedia("(forced-colors: active)").matches
                ? {
                    background: AppOptions.get("pageColorsBackground"),
                    foreground: AppOptions.get("pageColorsForeground"),
                }
                : null;
        let altTextManager;
        // NOTE
        // if (AppOptions.get("enableUpdatedAddImage")) {
        //   altTextManager = appConfig.newAltTextDialog
        //     ? new NewAltTextManager(
        //         appConfig.newAltTextDialog,
        //         this.overlayManager,
        //         eventBus
        //       )
        //     : null;
        // } else {
        //   altTextManager = appConfig.altTextDialog
        //     ? new AltTextManager(
        //         appConfig.altTextDialog,
        //         container,
        //         this.overlayManager,
        //         eventBus
        //       )
        //     : null;
        // }

        const enableHWA = AppOptions.get("enableHWA");
        const pdfViewer = new PDFViewer({
            container,
            viewer,
            eventBus,
            renderingQueue: pdfRenderingQueue,
            linkService: pdfLinkService,
            downloadManager,
            altTextManager,
            findController,
            scriptingManager:
                AppOptions.get("enableScripting") && pdfScriptingManager,
            l10n,
            textLayerMode: AppOptions.get("textLayerMode"),
            annotationMode: AppOptions.get("annotationMode"),
            annotationEditorMode,
            annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
            enableHighlightFloatingButton: AppOptions.get(
                "enableHighlightFloatingButton"
            ),
            enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"),
            enableNewAltTextWhenAddingImage: AppOptions.get(
                "enableNewAltTextWhenAddingImage"
            ),
            imageResourcesPath: AppOptions.get("imageResourcesPath"),
            enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
            maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
            enablePermissions: AppOptions.get("enablePermissions"),
            pageColors,
            mlManager: this.mlManager,
            abortSignal: this._globalAbortController.signal,
            enableHWA,
        });
        this.pdfViewer = pdfViewer;

        pdfRenderingQueue.setViewer(pdfViewer);
        pdfLinkService.setViewer(pdfViewer);
        pdfScriptingManager.setViewer(pdfViewer);

        if (appConfig.sidebar?.thumbnailView) {
            this.pdfThumbnailViewer = new PDFThumbnailViewer({
                container: appConfig.sidebar.thumbnailView,
                eventBus,
                renderingQueue: pdfRenderingQueue,
                linkService: pdfLinkService,
                pageColors,
                abortSignal: this._globalAbortController.signal,
                enableHWA,
            });
            pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
        }

        // The browsing history is only enabled when the viewer is standalone,
        // i.e. not when it is embedded in a web page.
        if (!this.isViewerEmbedded && !AppOptions.get("disableHistory")) {
            this.pdfHistory = new PDFHistory({
                linkService: pdfLinkService,
                eventBus,
            });
            pdfLinkService.setHistory(this.pdfHistory);
        }

        if (!this.supportsIntegratedFind && appConfig.findBar) {
            this.findBar = new PDFFindBar(
                appConfig.findBar,
                appConfig.principalContainer,
                eventBus
            );
        }

        if (appConfig.annotationEditorParams) {
            if (
                ((typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
                    typeof AbortSignal.any === "function") &&
                annotationEditorMode !== AnnotationEditorType.DISABLE
            ) {
                this.annotationEditorParams = new AnnotationEditorParams(
                    appConfig.annotationEditorParams,
                    eventBus
                );
            } else {
                for (const id of ["editorModeButtons", "editorModeSeparator"]) {
                    document.getElementById(id)?.classList.add("hidden");
                }
            }
        }

        if (
            this.mlManager &&
            appConfig.secondaryToolbar?.imageAltTextSettingsButton
        ) {
            // NOTE
            // this.imageAltTextSettings = new ImageAltTextSettings(
            //   appConfig.altTextSettingsDialog,
            //   this.overlayManager,
            //   eventBus,
            //   this.mlManager
            // );
        }

        if (appConfig.documentProperties) {
            this.pdfDocumentProperties = new PDFDocumentProperties(
                appConfig.documentProperties,
                this.overlayManager,
                eventBus,
                l10n,
                /* fileNameLookup = */ () => this._docFilename
            );
        }

        // NOTE: The cursor-tools are unlikely to be helpful/useful in GeckoView,
        // in particular the `HandTool` which basically simulates touch scrolling.
        if (appConfig.secondaryToolbar?.cursorHandToolButton) {
            this.pdfCursorTools = new PDFCursorTools({
                container,
                eventBus,
                cursorToolOnLoad: AppOptions.get("cursorToolOnLoad"),
            });
        }

        if (appConfig.toolbar) {
            if (
                typeof PDFJSDev === "undefined"
                    ? window.isGECKOVIEW
                    : PDFJSDev.test("GECKOVIEW")
            ) {
                const nimbusData = JSON.parse(
                    AppOptions.get("nimbusDataStr") || "null"
                );
                this.toolbar = new Toolbar(appConfig.toolbar, eventBus, nimbusData);
            } else {
                this.toolbar = new Toolbar(
                    appConfig.toolbar,
                    eventBus,
                    AppOptions.get("toolbarDensity")
                );
            }
        }

        if (appConfig.secondaryToolbar) {
            if (AppOptions.get("enableAltText")) {
                appConfig.secondaryToolbar.imageAltTextSettingsButton?.classList.remove(
                    "hidden"
                );
                appConfig.secondaryToolbar.imageAltTextSettingsSeparator?.classList.remove(
                    "hidden"
                );
            }

            this.secondaryToolbar = new SecondaryToolbar(
                appConfig.secondaryToolbar,
                eventBus
            );
        }

        if (
            this.supportsFullscreen &&
            appConfig.secondaryToolbar?.presentationModeButton
        ) {
            this.pdfPresentationMode = new PDFPresentationMode({
                container,
                pdfViewer,
                eventBus,
            });
        }

        if (appConfig.passwordOverlay) {
            this.passwordPrompt = new PasswordPrompt(
                appConfig.passwordOverlay,
                this.overlayManager,
                this.isViewerEmbedded
            );
        }

        if (appConfig.sidebar?.outlineView) {
            this.pdfOutlineViewer = new PDFOutlineViewer({
                container: appConfig.sidebar.outlineView,
                eventBus,
                l10n,
                linkService: pdfLinkService,
                downloadManager,
            });
        }

        if (appConfig.sidebar?.attachmentsView) {
            this.pdfAttachmentViewer = new PDFAttachmentViewer({
                container: appConfig.sidebar.attachmentsView,
                eventBus,
                l10n,
                downloadManager,
            });
        }

        if (appConfig.sidebar?.layersView) {
            this.pdfLayerViewer = new PDFLayerViewer({
                container: appConfig.sidebar.layersView,
                eventBus,
                l10n,
            });
        }

        if (appConfig.sidebar) {
            this.pdfSidebar = new PDFSidebar({
                elements: appConfig.sidebar,
                eventBus,
                l10n,
            });
            this.pdfSidebar.onToggled = this.forceRendering.bind(this);
            this.pdfSidebar.onUpdateThumbnails = () => {
                // Use the rendered pages to set the corresponding thumbnail images.
                for (const pageView of pdfViewer.getCachedPageViews()) {
                    if (pageView.renderingState === RenderingStates.FINISHED) {
                        this.pdfThumbnailViewer
                            .getThumbnail(pageView.id - 1)
                            ?.setImage(pageView);
                    }
                }
                this.pdfThumbnailViewer.scrollThumbnailIntoView(
                    pdfViewer.currentPageNumber
                );
            };
        }
    }

    async run(config) {
        await this.initialize(config);

        const {appConfig, eventBus} = this;
        let file;
        if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
            const queryString = document.location.search.substring(1);
            const params = parseQueryString(queryString);
            file = params.get("file") ?? AppOptions.get("defaultUrl");
            validateFileURL(file);
        } else if (PDFJSDev.test("MOZCENTRAL")) {
            file = window.location.href;
        } else if (PDFJSDev.test("CHROME")) {
            file = AppOptions.get("defaultUrl");
        }

        if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
            const fileInput = (this._openFileInput = document.createElement("input"));
            fileInput.id = "fileInput";
            fileInput.hidden = true;
            fileInput.type = "file";
            fileInput.value = null;
            document.body.append(fileInput);

            fileInput.addEventListener("change", function (evt) {
                const {files} = evt.target;
                if (!files || files.length === 0) {
                    return;
                }
                eventBus.dispatch("fileinputchange", {
                    source: this,
                    fileInput: evt.target,
                });
            });

            // Enable dragging-and-dropping a new PDF file onto the viewerContainer.
            appConfig.mainContainer.addEventListener("dragover", function (evt) {
                for (const item of evt.dataTransfer.items) {
                    if (item.type === "application/pdf") {
                        evt.dataTransfer.dropEffect =
                            evt.dataTransfer.effectAllowed === "copy" ? "copy" : "move";
                        evt.preventDefault();
                        evt.stopPropagation();
                        return;
                    }
                }
            });
            appConfig.mainContainer.addEventListener("drop", function (evt) {
                if (evt.dataTransfer.files?.[0].type !== "application/pdf") {
                    return;
                }
                evt.preventDefault();
                evt.stopPropagation();
                eventBus.dispatch("fileinputchange", {
                    source: this,
                    fileInput: evt.dataTransfer,
                });
            });
        }

        if (!AppOptions.get("supportsDocumentFonts")) {
            AppOptions.set("disableFontFace", true);
            this.l10n.get("pdfjs-web-fonts-disabled").then(msg => {
                console.warn(msg);
            });
        }

        if (!this.supportsPrinting) {
            appConfig.toolbar?.print?.classList.add("hidden");
            appConfig.secondaryToolbar?.printButton.classList.add("hidden");
        }

        if (!this.supportsFullscreen) {
            appConfig.secondaryToolbar?.presentationModeButton.classList.add(
                "hidden"
            );
        }

        if (this.supportsIntegratedFind) {
            appConfig.findBar?.toggleButton?.classList.add("hidden");
        }

        if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
            if (file) {
                this.open({url: file});
            } else {
                this._hideViewBookmark();
            }
        } else if (PDFJSDev.test("MOZCENTRAL || CHROME")) {
            this.setTitleUsingUrl(file, /* downloadUrl = */ file);

            this.externalServices.initPassiveLoading();
        } else {
            throw new Error("Not implemented: run");
        }
    }

    get externalServices() {
        return shadow(this, "externalServices", new ExternalServices());
    }

    get initialized() {
        return this._initializedCapability.settled;
    }

    get initializedPromise() {
        return this._initializedCapability.promise;
    }

    updateZoom(steps, scaleFactor, origin) {
        if (this.pdfViewer.isInPresentationMode) {
            return;
        }
        this.pdfViewer.updateScale({
            drawingDelay: AppOptions.get("defaultZoomDelay"),
            steps,
            scaleFactor,
            origin,
        });
    }

    zoomIn() {
        this.updateZoom(1);
    }

    zoomOut() {
        this.updateZoom(-1);
    }

    zoomReset() {
        if (this.pdfViewer.isInPresentationMode) {
            return;
        }
        this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE;
    }

    get pagesCount() {
        return this.pdfDocument ? this.pdfDocument.numPages : 0;
    }

    get page() {
        return this.pdfViewer.currentPageNumber;
    }

    set page(val) {
        this.pdfViewer.currentPageNumber = val;
    }

    get supportsPrinting() {
        return PDFPrintServiceFactory.supportsPrinting;
    }

    get supportsFullscreen() {
        return shadow(this, "supportsFullscreen", document.fullscreenEnabled);
    }

    get supportsPinchToZoom() {
        return shadow(
            this,
            "supportsPinchToZoom",
            AppOptions.get("supportsPinchToZoom")
        );
    }

    get supportsIntegratedFind() {
        return shadow(
            this,
            "supportsIntegratedFind",
            AppOptions.get("supportsIntegratedFind")
        );
    }

    get loadingBar() {
        // NOTE
        const barElement = this.appConfig.appContainer.querySelector("#loadingBar");
        const bar = barElement ? new ProgressBar(barElement) : null;
        return shadow(this, "loadingBar", bar);
    }

    get supportsMouseWheelZoomCtrlKey() {
        return shadow(
            this,
            "supportsMouseWheelZoomCtrlKey",
            AppOptions.get("supportsMouseWheelZoomCtrlKey")
        );
    }

    get supportsMouseWheelZoomMetaKey() {
        return shadow(
            this,
            "supportsMouseWheelZoomMetaKey",
            AppOptions.get("supportsMouseWheelZoomMetaKey")
        );
    }

    get supportsCaretBrowsingMode() {
        return AppOptions.get("supportsCaretBrowsingMode");
    }

    moveCaret(isUp, select) {
        this._caretBrowsing ||= new CaretBrowsingMode(
            this._globalAbortController.signal,
            this.appConfig.mainContainer,
            this.appConfig.viewerContainer,
            this.appConfig.toolbar?.container
        );
        this._caretBrowsing.moveCaret(isUp, select);
    }

    setTitleUsingUrl(url = "", downloadUrl = null) {
        this.url = url;
        this.baseUrl = url.split("#", 1)[0];
        if (downloadUrl) {
            this._downloadUrl =
                downloadUrl === url ? this.baseUrl : downloadUrl.split("#", 1)[0];
        }
        if (isDataScheme(url)) {
            this._hideViewBookmark();
        } else if (
            typeof PDFJSDev !== "undefined" &&
            PDFJSDev.test("MOZCENTRAL || CHROME")
        ) {
            AppOptions.set("docBaseUrl", this.baseUrl);
        }

        let title = getPdfFilenameFromUrl(url, "");
        if (!title) {
            try {
                title = decodeURIComponent(getFilenameFromUrl(url));
            } catch {
                // decodeURIComponent may throw URIError.
            }
        }
        this.setTitle(title || url); // Always fallback to the raw URL.
    }

    setTitle(title = this._title) {
        this._title = title;

        if (this.isViewerEmbedded) {
            // Embedded PDF viewers should not be changing their parent page's title.
            return;
        }
        const editorIndicator =
            this._hasAnnotationEditors && !this.pdfRenderingQueue.printing;
        document.title = `${editorIndicator ? "* " : ""}${title}`;
    }

    get _docFilename() {
        // Use `this.url` instead of `this.baseUrl` to perform filename detection
        // based on the reference fragment as ultimate fallback if needed.
        return this._contentDispositionFilename || getPdfFilenameFromUrl(this.url);
    }

    /**
     * @private
     */
    _hideViewBookmark() {
        const {secondaryToolbar} = this.appConfig;
        // URL does not reflect proper document location - hiding some buttons.
        // NOTE
        secondaryToolbar?.viewBookmarkButton.classList.add("fn__hidden");

        // Avoid displaying multiple consecutive separators in the secondaryToolbar.
        if (secondaryToolbar?.presentationModeButton.classList.contains("fn__hidden")) {
            document.getElementById("viewBookmarkSeparator")?.classList.add("fn__hidden");
        }
    }

    /**
     * Closes opened PDF document.
     * @returns {Promise} - Returns the promise, which is resolved when all
     *                      destruction is completed.
     */
    async close() {
        this._unblockDocumentLoadEvent();
        this._hideViewBookmark();

        if (!this.pdfLoadingTask) {
            return;
        }
        if (
            (typeof PDFJSDev === "undefined" ||
                PDFJSDev.test("GENERIC && !TESTING")) &&
            this.pdfDocument?.annotationStorage.size > 0 &&
            this._annotationStorageModified
        ) {
            try {
                // Trigger saving, to prevent data loss in forms; see issue 12257.
                await this.save();
            } catch {
                // Ignoring errors, to ensure that document closing won't break.
            }
        }
        const promises = [];

        promises.push(this.pdfLoadingTask.destroy());
        this.pdfLoadingTask = null;

        if (this.pdfDocument) {
            this.pdfDocument = null;

            this.pdfThumbnailViewer?.setDocument(null);
            this.pdfViewer.setDocument(null);
            this.pdfLinkService.setDocument(null);
            this.pdfDocumentProperties?.setDocument(null);
        }
        this.pdfLinkService.externalLinkEnabled = true;
        this.store = null;
        this.isInitialViewSet = false;
        this.url = "";
        this.baseUrl = "";
        this._downloadUrl = "";
        this.documentInfo = null;
        this.metadata = null;
        this._contentDispositionFilename = null;
        this._contentLength = null;
        this._saveInProgress = false;
        this._hasAnnotationEditors = false;

        promises.push(
            this.pdfScriptingManager.destroyPromise,
            this.passwordPrompt.close()
        );

        this.setTitle();
        this.pdfSidebar?.reset();
        this.pdfOutlineViewer?.reset();
        this.pdfAttachmentViewer?.reset();
        this.pdfLayerViewer?.reset();

        this.pdfHistory?.reset();
        this.findBar?.reset();
        this.toolbar?.reset();
        this.secondaryToolbar?.reset();
        this._PDFBug?.cleanup();

        await Promise.all(promises);
    }

    /**
     * Opens a new PDF document.
     * @param {Object} args - Accepts any/all of the properties from
     *   {@link DocumentInitParameters}, and also a `originalUrl` string.
     * @returns {Promise} - Promise that is resolved when the document is opened.
     */
    async open(args) {
        if (this.pdfLoadingTask) {
            // We need to destroy already opened document.
            await this.close();
        }
        // Set the necessary global worker parameters, using the available options.
        const workerParams = AppOptions.getAll(OptionKind.WORKER);
        Object.assign(GlobalWorkerOptions, workerParams);

        if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
            if (args.data && isPdfFile(args.filename)) {
                this._contentDispositionFilename = args.filename;
            }
        } else if (args.url) {
            // The Firefox built-in viewer always calls `setTitleUsingUrl`, before
            // `initPassiveLoading`, and it never provides an `originalUrl` here.
            this.setTitleUsingUrl(
                args.originalUrl || args.url,
                /* downloadUrl = */ args.url
            );
        }

        // Set the necessary API parameters, using all the available options.
        const apiParams = AppOptions.getAll(OptionKind.API);
        const loadingTask = getDocument({
            ...apiParams,
            ...args,
        });
        this.pdfLoadingTask = loadingTask;

        loadingTask.onPassword = (updateCallback, reason) => {
            if (this.isViewerEmbedded) {
                // The load event can't be triggered until the password is entered, so
                // if the viewer is in an iframe and its visibility depends on the
                // onload callback then the viewer never shows (bug 1801341).
                this._unblockDocumentLoadEvent();
            }

            this.pdfLinkService.externalLinkEnabled = false;
            this.passwordPrompt.setUpdateCallback(updateCallback, reason);
            this.passwordPrompt.open();
        };

        loadingTask.onProgress = ({loaded, total}) => {
            this.progress(loaded / total);
        };

        return loadingTask.promise.then(
            pdfDocument => {
                this.load(pdfDocument);
            },
            reason => {
                if (loadingTask !== this.pdfLoadingTask) {
                    return undefined; // Ignore errors for previously opened PDF files.
                }

                // NOTE
                let key = "loadingError";
                if (reason instanceof InvalidPDFException) {
                    key = "invalidFileError";
                } else if (reason instanceof MissingPDFException) {
                    key = "missingFileError";
                } else if (reason instanceof UnexpectedResponseException) {
                    key = "unexpectedResponseError";
                }
                return this._documentError(window.siyuan.languages[key], {message: reason.message}).then(
                    () => {
                        throw reason;
                    }
                );
            }
        );
    }

    async download() {
        let data;
        try {
            data = await this.pdfDocument.getData();
        } catch {
            // When the PDF document isn't ready, simply download using the URL.
        }
        this.downloadManager.download(data, this._downloadUrl, this._docFilename);
    }

    async save() {
        if (this._saveInProgress) {
            return;
        }
        this._saveInProgress = true;
        await this.pdfScriptingManager.dispatchWillSave();

        try {
            const data = await this.pdfDocument.saveDocument();
            this.downloadManager.download(data, this._downloadUrl, this._docFilename);
        } catch (reason) {
            // When the PDF document isn't ready, fallback to a "regular" download.
            console.error(`Error when saving the document: ${reason.message}`);
            await this.download();
        } finally {
            await this.pdfScriptingManager.dispatchDidSave();
            this._saveInProgress = false;
        }

        if (this._hasAnnotationEditors) {
            this.externalServices.reportTelemetry({
                type: "editing",
                data: {
                    type: "save",
                    stats: this.pdfDocument?.annotationStorage.editorStats,
                },
            });
        }
    }

    async downloadOrSave() {
        // In the Firefox case, this method MUST always trigger a download.
        // When the user is closing a modified and unsaved document, we display a
        // prompt asking for saving or not. In case they save, we must wait for
        // saving to complete before closing the tab.
        // So in case this function does not trigger a download, we must trigger a
        // a message and change PdfjsChild.sys.mjs to take it into account.
        const {classList} = this.appConfig.appContainer;
        classList.add("wait");
        await (this.pdfDocument?.annotationStorage.size > 0
            ? this.save()
            : this.download());
        classList.remove("wait");
    }

    /**
     * Report the error; used for errors affecting loading and/or parsing of
     * the entire PDF document.
     */
    async _documentError(key, moreInfo = null) {
        this._unblockDocumentLoadEvent();

        const message = await this._otherError(
            key || "pdfjs-loading-error",
            moreInfo
        );

        this.eventBus.dispatch("documenterror", {
            source: this,
            message,
            reason: moreInfo?.message ?? null,
        });
    }

    /**
     * Report the error; used for errors affecting e.g. only a single page.
     * @param {string} key - The localization key for the error.
     * @param {Object} [moreInfo] - Further information about the error that is
     *                              more technical. Should have a 'message' and
     *                              optionally a 'stack' property.
     * @returns {string} A (localized) error message that is human readable.
     */
    async _otherError(key, moreInfo = null) {
        // NOTE
        const message = window.siyuan.languages[key] || key;

        const moreInfoText = [`PDF.js v${version || "?"} (build: ${build || "?"})`];
        if (moreInfo) {
            moreInfoText.push(`Message: ${moreInfo.message}`);

            if (moreInfo.stack) {
                moreInfoText.push(`Stack: ${moreInfo.stack}`);
            } else {
                if (moreInfo.filename) {
                    moreInfoText.push(`File: ${moreInfo.filename}`);
                }
                if (moreInfo.lineNumber) {
                    moreInfoText.push(`Line: ${moreInfo.lineNumber}`);
                }
            }
        }

        console.error(`${message}\n\n${moreInfoText.join("\n")}`);
        return message;
    }

    progress(level) {
        const percent = Math.round(level * 100);
        // When we transition from full request to range requests, it's possible
        // that we discard some of the loaded data. This can cause the loading
        // bar to move backwards. So prevent this by only updating the bar if it
        // increases.
        if (!this.loadingBar || percent <= this.loadingBar.percent) {
            return;
        }
        this.loadingBar.percent = percent;

        // When disableAutoFetch is enabled, it's not uncommon for the entire file
        // to never be fetched (depends on e.g. the file structure). In this case
        // the loading bar will not be completely filled, nor will it be hidden.
        // To prevent displaying a partially filled loading bar permanently, we
        // hide it when no data has been loaded during a certain amount of time.
        if (
            this.pdfDocument?.loadingParams.disableAutoFetch ??
            AppOptions.get("disableAutoFetch")
        ) {
            this.loadingBar.setDisableAutoFetch();
        }
    }

    load(pdfDocument) {
        this.pdfDocument = pdfDocument;

        pdfDocument.getDownloadInfo().then(({length}) => {
            this._contentLength = length; // Ensure that the correct length is used.
            this.loadingBar?.hide();

            firstPagePromise.then(() => {
                this.eventBus.dispatch("documentloaded", {source: this});
            });
        });

        // Since the `setInitialView` call below depends on this being resolved,
        // fetch it early to avoid delaying initial rendering of the PDF document.
        const pageLayoutPromise = pdfDocument.getPageLayout().catch(() => {
            /* Avoid breaking initial rendering; ignoring errors. */
        });
        const pageModePromise = pdfDocument.getPageMode().catch(() => {
            /* Avoid breaking initial rendering; ignoring errors. */
        });
        const openActionPromise = pdfDocument.getOpenAction().catch(() => {
            /* Avoid breaking initial rendering; ignoring errors. */
        });

        this.toolbar?.setPagesCount(pdfDocument.numPages, false);
        this.secondaryToolbar?.setPagesCount(pdfDocument.numPages);

        if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME")) {
            const baseUrl = location.href.split("#", 1)[0];
            // Ignore "data:"-URLs for performance reasons, even though it may cause
            // internal links to not work perfectly in all cases (see bug 1803050).
            this.pdfLinkService.setDocument(
                pdfDocument,
                isDataScheme(baseUrl) ? null : baseUrl
            );
        } else {
            this.pdfLinkService.setDocument(pdfDocument);
        }
        this.pdfDocumentProperties?.setDocument(pdfDocument);

        const pdfViewer = this.pdfViewer;
        pdfViewer.setDocument(pdfDocument);
        const {firstPagePromise, onePageRendered, pagesPromise} = pdfViewer;

        this.pdfThumbnailViewer?.setDocument(pdfDocument);

        const storedPromise = (this.store = new ViewHistory(
            pdfDocument.fingerprints[0]
        ))
            .getMultiple({
                page: null,
                zoom: DEFAULT_SCALE_VALUE,
                scrollLeft: "0",
                scrollTop: "0",
                rotation: null,
                sidebarView: SidebarView.UNKNOWN,
                scrollMode: ScrollMode.UNKNOWN,
                spreadMode: SpreadMode.UNKNOWN,
            })
            .catch(() => {
                /* Unable to read from storage; ignoring errors. */
            });

        firstPagePromise.then(pdfPage => {
            this.loadingBar?.setWidth(this.appConfig.viewerContainer);
            this._initializeAnnotationStorageCallbacks(pdfDocument);

            Promise.all([
                animationStarted,
                storedPromise,
                pageLayoutPromise,
                pageModePromise,
                openActionPromise,
            ])
                .then(async ([timeStamp, stored, pageLayout, pageMode, openAction]) => {
                    const viewOnLoad = AppOptions.get("viewOnLoad");

                    this._initializePdfHistory({
                        fingerprint: pdfDocument.fingerprints[0],
                        viewOnLoad,
                        initialDest: openAction?.dest,
                    });
                    const initialBookmark = this.initialBookmark;

                    // Initialize the default values, from user preferences.
                    const zoom = AppOptions.get("defaultZoomValue");
                    let hash = zoom ? `zoom=${zoom}` : null;

                    let rotation = null;
                    let sidebarView = AppOptions.get("sidebarViewOnLoad");
                    let scrollMode = AppOptions.get("scrollModeOnLoad");
                    let spreadMode = AppOptions.get("spreadModeOnLoad");
                    // NOTE
                    stored.page = this.pdfId || stored.page;
                    if (stored?.page && viewOnLoad !== ViewOnLoad.INITIAL) {
                        hash =
                            `page=${stored.page}&zoom=${zoom || stored.zoom},` +
                            // NOTE https://github.com/siyuan-note/siyuan/issues/12291
                            `${stored.scrollLeft}${this.pdfId ? "" : "," + stored.scrollTop}`;

                        rotation = parseInt(stored.rotation, 10);
                        // Always let user preference take precedence over the view history.
                        if (sidebarView === SidebarView.UNKNOWN) {
                            sidebarView = stored.sidebarView | 0;
                        }
                        if (scrollMode === ScrollMode.UNKNOWN) {
                            scrollMode = stored.scrollMode | 0;
                        }
                        if (spreadMode === SpreadMode.UNKNOWN) {
                            spreadMode = stored.spreadMode | 0;
                        }
                    }
                    // NOTE 定位分页，最后通过 showHighlight 进行高亮
                    if (hash.indexOf("page=") === -1 && this.pdfId) {
                        hash += `&page=${this.pdfId}`;
                    }
                    // Always let the user preference/view history take precedence.
                    if (pageMode && sidebarView === SidebarView.UNKNOWN) {
                        sidebarView = apiPageModeToSidebarView(pageMode);
                    }
                    if (
                        pageLayout &&
                        scrollMode === ScrollMode.UNKNOWN &&
                        spreadMode === SpreadMode.UNKNOWN
                    ) {
                        const modes = apiPageLayoutToViewerModes(pageLayout);
                        // TODO: Try to improve page-switching when using the mouse-wheel
                        // and/or arrow-keys before allowing the document to control this.
                        // scrollMode = modes.scrollMode;
                        spreadMode = modes.spreadMode;
                    }

                    this.setInitialView(hash, {
                        rotation,
                        sidebarView,
                        scrollMode,
                        spreadMode,
                    });
                    this.eventBus.dispatch("documentinit", {source: this});
                    // Make all navigation keys work on document load,
                    // unless the viewer is embedded in a web page.
                    if (!this.isViewerEmbedded) {
                        pdfViewer.focus();
                    }

                    // For documents with different page sizes, once all pages are
                    // resolved, ensure that the correct location becomes visible on load.
                    // (To reduce the risk, in very large and/or slow loading documents,
                    //  that the location changes *after* the user has started interacting
                    //  with the viewer, wait for either `pagesPromise` or a timeout.)
                    await Promise.race([
                        pagesPromise,
                        new Promise(resolve => {
                            setTimeout(resolve, FORCE_PAGES_LOADED_TIMEOUT);
                        }),
                    ]);
                    if (!initialBookmark && !hash) {
                        return;
                    }
                    if (pdfViewer.hasEqualPageSizes) {
                        return;
                    }
                    this.initialBookmark = initialBookmark;

                    // eslint-disable-next-line no-self-assign
                    pdfViewer.currentScaleValue = pdfViewer.currentScaleValue;
                    // Re-apply the initial document location.
                    this.setInitialView(hash);
                })
                .catch(() => {
                    // Ensure that the document is always completely initialized,
                    // even if there are any errors thrown above.
                    this.setInitialView();
                })
                .then(function () {
                    // At this point, rendering of the initial page(s) should always have
                    // started (and may even have completed).
                    // To prevent any future issues, e.g. the document being completely
                    // blank on load, always trigger rendering here.
                    pdfViewer.update();
                    // NOTE: 没有渲染完就切换页签导致 https://ld246.com/article/1677072688346
                    const tabElement = hasClosestByClassName(pdfViewer.container, "fn__flex-1")
                    if (tabElement) {
                        tabElement.removeAttribute("data-loading")
                    }
                });
        });

        pagesPromise.then(
            () => {
                this._unblockDocumentLoadEvent();

                this._initializeAutoPrint(pdfDocument, openActionPromise);
            },
            reason => {
                this._documentError("pdfjs-loading-error", {message: reason.message});
            }
        );

        onePageRendered.then(data => {
            this.externalServices.reportTelemetry({
                type: "pageInfo",
                timestamp: data.timestamp,
            });

            if (this.pdfOutlineViewer) {
                pdfDocument.getOutline().then(outline => {
                    if (pdfDocument !== this.pdfDocument) {
                        return; // The document was closed while the outline resolved.
                    }
                    this.pdfOutlineViewer.render({outline, pdfDocument});
                });
            }
            if (this.pdfAttachmentViewer) {
                pdfDocument.getAttachments().then(attachments => {
                    if (pdfDocument !== this.pdfDocument) {
                        return; // The document was closed while the attachments resolved.
                    }
                    this.pdfAttachmentViewer.render({attachments});
                });
            }
            if (this.pdfLayerViewer) {
                // Ensure that the layers accurately reflects the current state in the
                // viewer itself, rather than the default state provided by the API.
                pdfViewer.optionalContentConfigPromise.then(optionalContentConfig => {
                    if (pdfDocument !== this.pdfDocument) {
                        return; // The document was closed while the layers resolved.
                    }
                    this.pdfLayerViewer.render({optionalContentConfig, pdfDocument});
                });
            }
        });

        this._initializePageLabels(pdfDocument);
        this._initializeMetadata(pdfDocument);
    }

    /**
     * @private
     */
    async _scriptingDocProperties(pdfDocument) {
        if (!this.documentInfo) {
            // It should be *extremely* rare for metadata to not have been resolved
            // when this code runs, but ensure that we handle that case here.
            await new Promise(resolve => {
                this.eventBus._on("metadataloaded", resolve, {once: true});
            });
            if (pdfDocument !== this.pdfDocument) {
                return null; // The document was closed while the metadata resolved.
            }
        }
        if (!this._contentLength) {
            // Always waiting for the entire PDF document to be loaded will, most
            // likely, delay sandbox-creation too much in the general case for all
            // PDF documents which are not provided as binary data to the API.
            // Hence we'll simply have to trust that the `contentLength` (as provided
            // by the server), when it exists, is accurate enough here.
            await new Promise(resolve => {
                this.eventBus._on("documentloaded", resolve, {once: true});
            });
            if (pdfDocument !== this.pdfDocument) {
                return null; // The document was closed while the downloadInfo resolved.
            }
        }

        return {
            ...this.documentInfo,
            baseURL: this.baseUrl,
            filesize: this._contentLength,
            filename: this._docFilename,
            metadata: this.metadata?.getRaw(),
            authors: this.metadata?.get("dc:creator"),
            numPages: this.pagesCount,
            URL: this.url,
        };
    }

    /**
     * @private
     */
    async _initializeAutoPrint(pdfDocument, openActionPromise) {
        const [openAction, jsActions] = await Promise.all([
            openActionPromise,
            this.pdfViewer.enableScripting ? null : pdfDocument.getJSActions(),
        ]);

        if (pdfDocument !== this.pdfDocument) {
            return; // The document was closed while the auto print data resolved.
        }
        let triggerAutoPrint = openAction?.action === "Print";

        if (jsActions) {
            console.warn("Warning: JavaScript support is not enabled");

            // Hack to support auto printing.
            for (const name in jsActions) {
                if (triggerAutoPrint) {
                    break;
                }
                switch (name) {
                    case "WillClose":
                    case "WillSave":
                    case "DidSave":
                    case "WillPrint":
                    case "DidPrint":
                        continue;
                }
                triggerAutoPrint = jsActions[name].some(js => AutoPrintRegExp.test(js));
            }
        }

        if (triggerAutoPrint) {
            this.triggerPrinting();
        }
    }

    /**
     * @private
     */
    async _initializeMetadata(pdfDocument) {
        const {info, metadata, contentDispositionFilename, contentLength} =
            await pdfDocument.getMetadata();

        if (pdfDocument !== this.pdfDocument) {
            return; // The document was closed while the metadata resolved.
        }
        this.documentInfo = info;
        this.metadata = metadata;
        this._contentDispositionFilename ??= contentDispositionFilename;
        this._contentLength ??= contentLength; // See `getDownloadInfo`-call above.

        // Provides some basic debug information
        // NOTE
        // console.log(
        //   `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` +
        //     `${(info.Producer || "-").trim()} / ${(info.Creator || "-").trim()}] ` +
        //     `(PDF.js: ${version || "?"} [${build || "?"}])`
        // );
        let pdfTitle = info.Title;

        const metadataTitle = metadata?.get("dc:title");
        if (metadataTitle) {
            // Ghostscript can produce invalid 'dc:title' Metadata entries:
            //  - The title may be "Untitled" (fixes bug 1031612).
            //  - The title may contain incorrectly encoded characters, which thus
            //    looks broken, hence we ignore the Metadata entry when it contains
            //    characters from the Specials Unicode block (fixes bug 1605526).
            if (
                metadataTitle !== "Untitled" &&
                !/[\uFFF0-\uFFFF]/g.test(metadataTitle)
            ) {
                pdfTitle = metadataTitle;
            }
        }
        if (pdfTitle) {
            this.setTitle(
                `${pdfTitle} - ${this._contentDispositionFilename || this._title}`
            );
        } else if (this._contentDispositionFilename) {
            this.setTitle(this._contentDispositionFilename);
        }

        if (
            info.IsXFAPresent &&
            !info.IsAcroFormPresent &&
            !pdfDocument.isPureXfa
        ) {
            if (pdfDocument.loadingParams.enableXfa) {
                console.warn("Warning: XFA Foreground documents are not supported");
            } else {
                console.warn("Warning: XFA support is not enabled");
            }
        } else if (
            (info.IsAcroFormPresent || info.IsXFAPresent) &&
            !this.pdfViewer.renderForms
        ) {
            console.warn("Warning: Interactive form support is not enabled");
        }

        if (info.IsSignaturesPresent) {
            console.warn("Warning: Digital signatures validation is not supported");
        }

        this.eventBus.dispatch("metadataloaded", {source: this});
    }

    /**
     * @private
     */
    async _initializePageLabels(pdfDocument) {
        if (
            typeof PDFJSDev === "undefined"
                ? window.isGECKOVIEW
                : PDFJSDev.test("GECKOVIEW")
        ) {
            return;
        }
        const labels = await pdfDocument.getPageLabels();

        if (pdfDocument !== this.pdfDocument) {
            return; // The document was closed while the page labels resolved.
        }
        if (!labels || AppOptions.get("disablePageLabels")) {
            return;
        }
        const numLabels = labels.length;
        // Ignore page labels that correspond to standard page numbering,
        // or page labels that are all empty.
        let standardLabels = 0,
            emptyLabels = 0;
        for (let i = 0; i < numLabels; i++) {
            const label = labels[i];
            if (label === (i + 1).toString()) {
                standardLabels++;
            } else if (label === "") {
                emptyLabels++;
            } else {
                break;
            }
        }
        if (standardLabels >= numLabels || emptyLabels >= numLabels) {
            return;
        }
        const {pdfViewer, pdfThumbnailViewer, toolbar} = this;

        pdfViewer.setPageLabels(labels);
        pdfThumbnailViewer?.setPageLabels(labels);

        // Changing toolbar page display to use labels and we need to set
        // the label of the current page.
        toolbar?.setPagesCount(numLabels, true);
        toolbar?.setPageNumber(
            pdfViewer.currentPageNumber,
            pdfViewer.currentPageLabel
        );
    }

    /**
     * @private
     */
    _initializePdfHistory({fingerprint, viewOnLoad, initialDest = null}) {
        if (!this.pdfHistory) {
            return;
        }
        this.pdfHistory.initialize({
            fingerprint,
            resetHistory: viewOnLoad === ViewOnLoad.INITIAL,
            updateUrl: AppOptions.get("historyUpdateUrl"),
        });

        if (this.pdfHistory.initialBookmark) {
            this.initialBookmark = this.pdfHistory.initialBookmark;

            this.initialRotation = this.pdfHistory.initialRotation;
        }

        // Always let the browser history/document hash take precedence.
        if (
            initialDest &&
            !this.initialBookmark &&
            viewOnLoad === ViewOnLoad.UNKNOWN
        ) {
            this.initialBookmark = JSON.stringify(initialDest);
            // TODO: Re-factor the `PDFHistory` initialization to remove this hack
            // that's currently necessary to prevent weird initial history state.
            this.pdfHistory.push({explicitDest: initialDest, pageNumber: null});
        }
    }

    /**
     * @private
     */
    _initializeAnnotationStorageCallbacks(pdfDocument) {
        if (pdfDocument !== this.pdfDocument) {
            return;
        }
        const {annotationStorage} = pdfDocument;

        annotationStorage.onSetModified = () => {
            // NOTE
            // window.addEventListener("beforeunload", beforeUnload);

            if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
                this._annotationStorageModified = true;
            }
        };
        annotationStorage.onResetModified = () => {
            // NOTE
            // window.removeEventListener("beforeunload", beforeUnload);

            if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
                delete this._annotationStorageModified;
            }
        };
        annotationStorage.onAnnotationEditor = typeStr => {
            this._hasAnnotationEditors = !!typeStr;
            this.setTitle();
        };
    }

    setInitialView(
        storedHash,
        {rotation, sidebarView, scrollMode, spreadMode} = {}
    ) {
        const setRotation = angle => {
            if (isValidRotation(angle)) {
                this.pdfViewer.pagesRotation = angle;
            }
        };
        const setViewerModes = (scroll, spread) => {
            if (isValidScrollMode(scroll)) {
                this.pdfViewer.scrollMode = scroll;
            }
            if (isValidSpreadMode(spread)) {
                this.pdfViewer.spreadMode = spread;
            }
        };
        this.isInitialViewSet = true;
        this.pdfSidebar?.setInitialView(sidebarView);

        setViewerModes(scrollMode, spreadMode);

        if (this.initialBookmark) {
            setRotation(this.initialRotation);
            delete this.initialRotation;

            this.pdfLinkService.setHash(this.initialBookmark);
            this.initialBookmark = null;
        } else if (storedHash) {
            setRotation(rotation);

            this.pdfLinkService.setHash(storedHash);
        }

        // Ensure that the correct page number is displayed in the UI,
        // even if the active page didn't change during document load.
        this.toolbar?.setPageNumber(
            this.pdfViewer.currentPageNumber,
            this.pdfViewer.currentPageLabel
        );
        this.secondaryToolbar?.setPageNumber(this.pdfViewer.currentPageNumber);

        if (!this.pdfViewer.currentScaleValue) {
            // Scale was not initialized: invalid bookmark or scale was not specified.
            // Setting the default one.
            this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE;
        }
    }

    /**
     * @private
     */
    _cleanup() {
        if (!this.pdfDocument) {
            return; // run cleanup when document is loaded
        }
        this.pdfViewer.cleanup();
        this.pdfThumbnailViewer?.cleanup();

        this.pdfDocument.cleanup(
            /* keepLoadedFonts = */ AppOptions.get("fontExtraProperties")
        );
    }

    forceRendering() {
        this.pdfRenderingQueue.printing = !!this.printService;
        this.pdfRenderingQueue.isThumbnailViewEnabled =
            this.pdfSidebar?.visibleView === SidebarView.THUMBS;
        this.pdfRenderingQueue.renderHighestPriority();
    }

    beforePrint() {
        this._printAnnotationStoragePromise = this.pdfScriptingManager
            .dispatchWillPrint()
            .catch(() => {
                /* Avoid breaking printing; ignoring errors. */
            })
            .then(() => this.pdfDocument?.annotationStorage.print);

        if (this.printService) {
            // There is no way to suppress beforePrint/afterPrint events,
            // but PDFPrintService may generate double events -- this will ignore
            // the second event that will be coming from native window.print().
            return;
        }

        if (!this.supportsPrinting) {
            // NOTE
            this._otherError(window.siyuan.languages.printingNotSupported);
            return;
        }

        // The beforePrint is a sync method and we need to know layout before
        // returning from this method. Ensure that we can get sizes of the pages.
        if (!this.pdfViewer.pageViewsReady) {
            this.l10n.get("pdfjs-printing-not-ready").then(msg => {
                // eslint-disable-next-line no-alert
                window.alert(msg);
            });
            return;
        }

        this.printService = PDFPrintServiceFactory.createPrintService({
            pdfDocument: this.pdfDocument,
            pagesOverview: this.pdfViewer.getPagesOverview(),
            printContainer: this.appConfig.printContainer,
            printResolution: AppOptions.get("printResolution"),
            printAnnotationStoragePromise: this._printAnnotationStoragePromise,
        });
        this.forceRendering();
        // Disable the editor-indicator during printing (fixes bug 1790552).
        this.setTitle();

        this.printService.layout();

        if (this._hasAnnotationEditors) {
            this.externalServices.reportTelemetry({
                type: "editing",
                data: {
                    type: "print",
                    stats: this.pdfDocument?.annotationStorage.editorStats,
                },
            });
        }
    }

    afterPrint() {
        if (this._printAnnotationStoragePromise) {
            this._printAnnotationStoragePromise.then(() => {
                this.pdfScriptingManager.dispatchDidPrint();
            });
            this._printAnnotationStoragePromise = null;
        }

        if (this.printService) {
            this.printService.destroy();
            this.printService = null;

            this.pdfDocument?.annotationStorage.resetModified();
        }
        this.forceRendering();
        // Re-enable the editor-indicator after printing (fixes bug 1790552).
        this.setTitle();
    }

    rotatePages(delta) {
        this.pdfViewer.pagesRotation += delta;
        // Note that the thumbnail viewer is updated, and rendering is triggered,
        // in the 'rotationchanging' event handler.
    }

    requestPresentationMode() {
        this.pdfPresentationMode?.request();
    }

    triggerPrinting() {
        if (this.supportsPrinting) {
            window.print();
        }
    }

    bindEvents() {
        if (this._eventBusAbortController) {
            return;
        }
        this._eventBusAbortController = new AbortController();

        const {
            eventBus,
            externalServices,
            pdfDocumentProperties,
            pdfViewer,
            preferences,
            _eventBusAbortController: {signal},
        } = this;

        eventBus._on("resize", onResize.bind(this), {signal});
        eventBus._on("hashchange", onHashchange.bind(this), {signal});
        eventBus._on("beforeprint", this.beforePrint.bind(this), {signal});
        eventBus._on("afterprint", this.afterPrint.bind(this), {signal});
        eventBus._on("pagerender", onPageRender.bind(this), {signal});
        eventBus._on("pagerendered", onPageRendered.bind(this), {signal});
        eventBus._on("updateviewarea", onUpdateViewarea.bind(this), {signal});
        eventBus._on("pagechanging", onPageChanging.bind(this), {signal});
        eventBus._on("scalechanging", onScaleChanging.bind(this), {signal});
        eventBus._on("rotationchanging", onRotationChanging.bind(this), {signal});
        eventBus._on("sidebarviewchanged", onSidebarViewChanged.bind(this), {
            signal,
        });
        eventBus._on("pagemode", onPageMode.bind(this), {signal});
        eventBus._on("namedaction", onNamedAction.bind(this), {signal});
        eventBus._on(
            "presentationmodechanged",
            evt => (pdfViewer.presentationModeState = evt.state),
            {signal}
        );
        eventBus._on("presentationmode", this.requestPresentationMode.bind(this), {
            signal,
        });
        eventBus._on(
            "switchannotationeditormode",
            evt => (pdfViewer.annotationEditorMode = evt),
            {signal}
        );
        eventBus._on("print", this.triggerPrinting.bind(this), {signal});
        eventBus._on("download", this.downloadOrSave.bind(this), {signal});
        eventBus._on("firstpage", () => (this.page = 1), {signal});
        eventBus._on("lastpage", () => (this.page = this.pagesCount), {signal});
        eventBus._on("nextpage", () => pdfViewer.nextPage(), {signal});
        eventBus._on("previouspage", () => pdfViewer.previousPage(), {signal});
        eventBus._on("zoomin", this.zoomIn.bind(this), {signal});
        eventBus._on("zoomout", this.zoomOut.bind(this), {signal});
        eventBus._on("zoomreset", this.zoomReset.bind(this), {signal});
        eventBus._on("pagenumberchanged", onPageNumberChanged.bind(this), {
            signal,
        });
        eventBus._on(
            "scalechanged",
            evt => (pdfViewer.currentScaleValue = evt.value),
            {signal}
        );
        eventBus._on("rotatecw", this.rotatePages.bind(this, 90), {signal});
        eventBus._on("rotateccw", this.rotatePages.bind(this, -90), {signal});
        eventBus._on(
            "optionalcontentconfig",
            evt => (pdfViewer.optionalContentConfigPromise = evt.promise),
            {signal}
        );
        eventBus._on("switchscrollmode", evt => (pdfViewer.scrollMode = evt.mode), {
            signal,
        });
        eventBus._on(
            "scrollmodechanged",
            onViewerModesChanged.bind(this, "scrollMode"),
            {signal}
        );
        eventBus._on("switchspreadmode", evt => (pdfViewer.spreadMode = evt.mode), {
            signal,
        });
        eventBus._on(
            "spreadmodechanged",
            onViewerModesChanged.bind(this, "spreadMode"),
            {signal}
        );
        eventBus._on("imagealttextsettings", onImageAltTextSettings.bind(this), {
            signal,
        });
        eventBus._on("documentproperties", () => pdfDocumentProperties?.open(), {
            signal,
        });
        eventBus._on("findfromurlhash", onFindFromUrlHash.bind(this), {signal});
        eventBus._on(
            "updatefindmatchescount",
            onUpdateFindMatchesCount.bind(this),
            {signal}
        );
        eventBus._on(
            "updatefindcontrolstate",
            onUpdateFindControlState.bind(this),
            {signal}
        );

        if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
            eventBus._on("fileinputchange", onFileInputChange.bind(this), {signal});
            eventBus._on("openfile", onOpenFile.bind(this), {signal});
        }
        if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
            eventBus._on(
                "annotationeditorstateschanged",
                evt => externalServices.updateEditorStates(evt),
                {signal}
            );
            eventBus._on(
                "reporttelemetry",
                evt => externalServices.reportTelemetry(evt.details),
                {signal}
            );
        }
        if (
            typeof PDFJSDev === "undefined" ||
            PDFJSDev.test("TESTING || MOZCENTRAL")
        ) {
            eventBus._on(
                "setpreference",
                evt => preferences.set(evt.name, evt.value),
                {signal}
            );
        }
    }

    bindWindowEvents() {
        if (this._windowAbortController) {
            return;
        }
        this._windowAbortController = new AbortController();

        const {
            eventBus,
            appConfig: {mainContainer},
            pdfViewer,
            _windowAbortController: {signal},
        } = this;

        function addWindowResolutionChange(evt = null) {
            if (evt) {
                pdfViewer.refresh();
            }
            const mediaQueryList = window.matchMedia(
                `(resolution: ${window.devicePixelRatio || 1}dppx)`
            );
            mediaQueryList.addEventListener("change", addWindowResolutionChange, {
                once: true,
                signal,
            });
        }

        addWindowResolutionChange();

        // NOTE
        const element = hasClosestByClassName(mainContainer, "pdf__outer");
        element.addEventListener("wheel", onWheel.bind(this), {
            passive: false,
            signal,
        });
        element.addEventListener("touchstart", onTouchStart.bind(this), {
            passive: false,
            signal,
        });
        element.addEventListener("touchmove", onTouchMove.bind(this), {
            passive: false,
            signal,
        });
        element.addEventListener("touchend", onTouchEnd.bind(this), {
            passive: false,
            signal,
        });
        element.addEventListener("click", onClick.bind(this), {signal});
        window.removeEventListener("keydown", onKeyDown, {signal});
        window.addEventListener("keydown", onKeyDown, {signal});
        window.removeEventListener("keyup", onKeyUp, {signal});
        window.addEventListener("keyup", onKeyUp, {signal});
        /*
        window.addEventListener(
          "resize",
          () => eventBus.dispatch("resize", { source: window }),
          { signal }
        );
        window.addEventListener(
          "hashchange",
          () => {
            eventBus.dispatch("hashchange", {
              source: window,
              hash: document.location.hash.substring(1),
            });
          },
          { signal }
        );
        window.addEventListener(
          "beforeprint",
          () => eventBus.dispatch("beforeprint", { source: window }),
          { signal }
        );
        window.addEventListener(
          "afterprint",
          () => eventBus.dispatch("afterprint", { source: window }),
          { signal }
        );
        window.addEventListener(
          "updatefromsandbox",
          evt => {
            eventBus.dispatch("updatefromsandbox", {
              source: window,
              detail: evt.detail,
            });
          },
          { signal }
        );*/

        if (
            (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
            !("onscrollend" in document.documentElement)
        ) {
            return;
        }
        if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
            // Using the values lastScrollTop and lastScrollLeft is a workaround to
            // https://bugzilla.mozilla.org/show_bug.cgi?id=1881974.
            // TODO: remove them once the bug is fixed.
            ({scrollTop: this._lastScrollTop, scrollLeft: this._lastScrollLeft} =
                mainContainer);
        }

        const scrollend = () => {
            if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
                ({scrollTop: this._lastScrollTop, scrollLeft: this._lastScrollLeft} =
                    mainContainer);
            }

            this._isScrolling = false;
            mainContainer.addEventListener("scroll", scroll, {
                passive: true,
                signal,
            });
            mainContainer.removeEventListener("scrollend", scrollend);
            mainContainer.removeEventListener("blur", scrollend);
        };
        const scroll = () => {
            if (this._isCtrlKeyDown) {
                return;
            }
            if (
                (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
                this._lastScrollTop === mainContainer.scrollTop &&
                this._lastScrollLeft === mainContainer.scrollLeft
            ) {
                return;
            }

            mainContainer.removeEventListener("scroll", scroll, {passive: true});
            this._isScrolling = true;
            mainContainer.addEventListener("scrollend", scrollend, {signal});
            mainContainer.addEventListener("blur", scrollend, {signal});
        };
        mainContainer.addEventListener("scroll", scroll, {
            passive: true,
            signal,
        });
    }

    unbindEvents() {
        this._eventBusAbortController?.abort();
        this._eventBusAbortController = null;
    }

    unbindWindowEvents() {
        this._windowAbortController?.abort();
        this._windowAbortController = null;
    }

    /**
     * @ignore
     */
    async testingClose() {
        this.unbindEvents();
        this.unbindWindowEvents();

        this._globalAbortController?.abort();
        this._globalAbortController = null;

        this.findBar?.close();

        await Promise.all([this.l10n?.destroy(), this.close()]);
    }

    _accumulateTicks(ticks, prop) {
        // If the direction changed, reset the accumulated ticks.
        if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) {
            this[prop] = 0;
        }
        this[prop] += ticks;
        const wholeTicks = Math.trunc(this[prop]);
        this[prop] -= wholeTicks;
        return wholeTicks;
    }

    _accumulateFactor(previousScale, factor, prop) {
        if (factor === 1) {
            return 1;
        }
        // If the direction changed, reset the accumulated factor.
        if ((this[prop] > 1 && factor < 1) || (this[prop] < 1 && factor > 1)) {
            this[prop] = 1;
        }

        const newFactor =
            Math.floor(previousScale * factor * this[prop] * 100) /
            (100 * previousScale);
        this[prop] = factor / newFactor;

        return newFactor;
    }

    /**
     * Should be called *after* all pages have loaded, or if an error occurred,
     * to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
     * @private
     */
    _unblockDocumentLoadEvent() {
        document.blockUnblockOnload?.(false);

        // Ensure that this method is only ever run once.
        this._unblockDocumentLoadEvent = () => {
        };
    }

    /**
     * Used together with the integration-tests, to enable awaiting full
     * initialization of the scripting/sandbox.
     */
    get scriptingReady() {
        return this.pdfScriptingManager.ready;
    }
};

initCom(PDFViewerApplication);

if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
    // NOTE
    // PDFPrintServiceFactory.initGlobals(PDFViewerApplication);
}

if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
    const HOSTED_VIEWER_ORIGINS = [
        "null",
        "http://mozilla.github.io",
        "https://mozilla.github.io",
    ];
    // eslint-disable-next-line no-var
    var validateFileURL = function (file) {
        if (!file) {
            return;
        }
        try {
            const viewerOrigin = new URL(window.location.href).origin || "null";
            if (HOSTED_VIEWER_ORIGINS.includes(viewerOrigin)) {
                // Hosted or local viewer, allow for any file locations
                return;
            }
            const fileOrigin = new URL(file, window.location.href).origin;
            // Removing of the following line will not guarantee that the viewer will
            // start accepting URLs from foreign origin -- CORS headers on the remote
            // server must be properly configured.
            if (fileOrigin !== viewerOrigin) {
                throw new Error("file origin does not match viewer's");
            }
        } catch (ex) {
            // NOTE
            console.log(window.siyuan.languages.loadingError, ex.message)
            // PDFViewerApplication._documentError("pdfjs-loading-error", {
            //   message: ex.message,
            // });
            throw ex;
        }
    };

    // eslint-disable-next-line no-var
    var onFileInputChange = function (evt) {
        if (this.pdfViewer?.isInPresentationMode) {
            return; // Opening a new PDF file isn't supported in Presentation Mode.
        }
        const file = evt.fileInput.files[0];

        this.open({
            url: URL.createObjectURL(file),
            originalUrl: file.name,
        });
    };

    // eslint-disable-next-line no-var
    var onOpenFile = function (evt) {
        this._openFileInput?.click();
    };
}

function onPageRender({pageNumber}) {
    // If the page is (the most) visible when it starts rendering,
    // ensure that the page number input loading indicator is displayed.
    if (pageNumber === this.page) {
        this.toolbar?.updateLoadingIndicatorState(true);
    }
}

function onPageRendered({pageNumber, error}) {
    // If the page is still visible when it has finished rendering,
    // ensure that the page number input loading indicator is hidden.
    if (pageNumber === this.page) {
        this.toolbar?.updateLoadingIndicatorState(false);
    }

    // Use the rendered page to set the corresponding thumbnail image.
    if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
        const pageView = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
        const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
            /* index = */ pageNumber - 1
        );
        if (pageView) {
            thumbnailView?.setImage(pageView);
        }
    }

    if (error) {
        this._otherError("pdfjs-rendering-error", error);
    }
}

function onPageMode({mode}) {
    // Handle the 'pagemode' hash parameter, see also `PDFLinkService_setHash`.
    let view;
    switch (mode) {
        case "thumbs":
            view = SidebarView.THUMBS;
            break;
        case "bookmarks":
        case "outline": // non-standard
            view = SidebarView.OUTLINE;
            break;
        case "attachments": // non-standard
            view = SidebarView.ATTACHMENTS;
            break;
        case "layers": // non-standard
            view = SidebarView.LAYERS;
            break;
        case "none":
            view = SidebarView.NONE;
            break;
        default:
            console.error('Invalid "pagemode" hash parameter: ' + mode);
            return;
    }
    this.pdfSidebar?.switchView(view, /* forceOpen = */ true);
}

function onNamedAction(evt) {
    // Processing a couple of named actions that might be useful, see also
    // `PDFLinkService.executeNamedAction`.
    switch (evt.action) {
        case "GoToPage":
            this.appConfig.toolbar?.pageNumber.select();
            break;

        case "Find":
            if (!this.supportsIntegratedFind) {
                this.findBar?.toggle();
            }
            break;

        case "Print":
            this.triggerPrinting();
            break;

        case "SaveAs":
            this.downloadOrSave();
            break;
    }
}

function onSidebarViewChanged({view}) {
    this.pdfRenderingQueue.isThumbnailViewEnabled = view === SidebarView.THUMBS;

    if (this.isInitialViewSet) {
        // Only update the storage when the document has been loaded *and* rendered.
        this.store?.set("sidebarView", view).catch(() => {
            // Unable to write to storage.
        });
    }
}

function onUpdateViewarea({location}) {
    if (this.isInitialViewSet) {
        // Only update the storage when the document has been loaded *and* rendered.
        this.store
            ?.setMultiple({
                page: location.pageNumber,
                zoom: location.scale,
                scrollLeft: location.left,
                scrollTop: location.top,
                rotation: location.rotation,
            })
            .catch(() => {
                // Unable to write to storage.
            });
    }
    if (this.appConfig.secondaryToolbar) {
        this.appConfig.secondaryToolbar.viewBookmarkButton.href =
            this.pdfLinkService.getAnchorUrl(location.pdfOpenParams);
    }
}

function onViewerModesChanged(name, evt) {
    if (this.isInitialViewSet && !this.pdfViewer.isInPresentationMode) {
        // Only update the storage when the document has been loaded *and* rendered.
        this.store?.set(name, evt.mode).catch(() => {
            // Unable to write to storage.
        });
    }
}

function onResize() {
    const {pdfDocument, pdfViewer, pdfRenderingQueue} = this;

    if (pdfRenderingQueue.printing && window.matchMedia("print").matches) {
        // Work-around issue 15324 by ignoring "resize" events during printing.
        return;
    }

    if (!pdfDocument) {
        return;
    }
    const currentScaleValue = pdfViewer.currentScaleValue;
    if (
        currentScaleValue === "auto" ||
        currentScaleValue === "page-fit" ||
        currentScaleValue === "page-width"
    ) {
        // Note: the scale is constant for 'page-actual'.
        pdfViewer.currentScaleValue = currentScaleValue;
    }
    pdfViewer.update();
}

function onHashchange(evt) {
    const hash = evt.hash;
    if (!hash) {
        return;
    }
    if (!this.isInitialViewSet) {
        this.initialBookmark = hash;
    } else if (!this.pdfHistory?.popStateInProgress) {
        this.pdfLinkService.setHash(hash);
    }
}

function onPageNumberChanged(evt) {
    // NOTE
    let it = this
    if (evt.pdfInstance) {
        it = evt.pdfInstance
    }
    const {pdfViewer} = it;
    // Note that for `<input type="number">` HTML elements, an empty string will
    // be returned for non-number inputs; hence we simply do nothing in that case.
    if (evt.value !== "") {
        it.pdfLinkService.goToPage(evt.value);
    }
    // NOTE
    if (evt.id) {
        hlPDFRect(it.pdfViewer.container, evt.id)
    }
    // Ensure that the page number input displays the correct value, even if the
    // value entered by the user was invalid (e.g. a floating point number).
    if (
        evt.value !== pdfViewer.currentPageNumber.toString() &&
        evt.value !== pdfViewer.currentPageLabel
    ) {
        it.toolbar?.setPageNumber(
            pdfViewer.currentPageNumber,
            pdfViewer.currentPageLabel
        );
    }
}

function onImageAltTextSettings() {
    this.imageAltTextSettings?.open({
        enableGuessAltText: AppOptions.get("enableGuessAltText"),
        enableNewAltTextWhenAddingImage: AppOptions.get(
            "enableNewAltTextWhenAddingImage"
        ),
    });
}

function onFindFromUrlHash(evt) {
    this.eventBus.dispatch("find", {
        source: evt.source,
        type: "",
        query: evt.query,
        caseSensitive: false,
        entireWord: false,
        highlightAll: true,
        findPrevious: false,
        matchDiacritics: true,
    });
}

function onUpdateFindMatchesCount({matchesCount}) {
    if (this.supportsIntegratedFind) {
        this.externalServices.updateFindMatchesCount(matchesCount);
    } else {
        this.findBar?.updateResultsCount(matchesCount);
    }
}

function onUpdateFindControlState({
                                      state,
                                      previous,
                                      entireWord,
                                      matchesCount,
                                      rawQuery,
                                  }) {
    if (this.supportsIntegratedFind) {
        this.externalServices.updateFindControlState({
            result: state,
            findPrevious: previous,
            entireWord,
            matchesCount,
            rawQuery,
        });
    } else {
        this.findBar?.updateUIState(state, previous, matchesCount);
    }
}

function onScaleChanging(evt) {
    this.toolbar?.setPageScale(evt.presetValue, evt.scale);

    this.pdfViewer.update();
}

function onRotationChanging(evt) {
    if (this.pdfThumbnailViewer) {
        this.pdfThumbnailViewer.pagesRotation = evt.pagesRotation;
    }

    this.forceRendering();
    // Ensure that the active page doesn't change during rotation.
    this.pdfViewer.currentPageNumber = evt.pageNumber;
}

function onPageChanging({pageNumber, pageLabel}) {
    this.toolbar?.setPageNumber(pageNumber, pageLabel);
    this.secondaryToolbar?.setPageNumber(pageNumber);

    if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
        this.pdfThumbnailViewer?.scrollThumbnailIntoView(pageNumber);
    }

    // Show/hide the loading indicator in the page number input element.
    const currentPage = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
    this.toolbar?.updateLoadingIndicatorState(
        currentPage?.renderingState === RenderingStates.RUNNING
    );
}

function onWheel(evt) {
    const {
        pdfViewer,
        supportsMouseWheelZoomCtrlKey,
        supportsMouseWheelZoomMetaKey,
        supportsPinchToZoom,
    } = this;

    if (pdfViewer.isInPresentationMode) {
        return;
    }

    // Pinch-to-zoom on a trackpad maps to a wheel event with ctrlKey set to true
    // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent#browser_compatibility
    // Hence if ctrlKey is true but ctrl key hasn't been pressed then we can
    // infer that we have a pinch-to-zoom.
    // But the ctrlKey could have been pressed outside of the browser window,
    // hence we try to do some magic to guess if the scaleFactor is likely coming
    // from a pinch-to-zoom or not.

    // It is important that we query deltaMode before delta{X,Y}, so that
    // Firefox doesn't switch to DOM_DELTA_PIXEL mode for compat with other
    // browsers, see https://bugzilla.mozilla.org/show_bug.cgi?id=1392460.
    const deltaMode = evt.deltaMode;

    // The following formula is a bit strange but it comes from:
    // https://searchfox.org/mozilla-central/rev/d62c4c4d5547064487006a1506287da394b64724/widget/InputData.cpp#618-626
    let scaleFactor = Math.exp(-evt.deltaY / 100);

    const isBuiltInMac =
        typeof PDFJSDev !== "undefined" &&
        PDFJSDev.test("MOZCENTRAL") &&
        FeatureTest.platform.isMac;
    const isPinchToZoom =
        evt.ctrlKey &&
        !this._isCtrlKeyDown &&
        deltaMode === WheelEvent.DOM_DELTA_PIXEL &&
        evt.deltaX === 0 &&
        (Math.abs(scaleFactor - 1) < 0.05 || isBuiltInMac) &&
        evt.deltaZ === 0;
    const origin = [evt.clientX, evt.clientY];

    if (
        isPinchToZoom ||
        (evt.ctrlKey && supportsMouseWheelZoomCtrlKey) ||
        (evt.metaKey && supportsMouseWheelZoomMetaKey)
    ) {
        // Only zoom the pages, not the entire viewer.
        evt.preventDefault();
        // NOTE: this check must be placed *after* preventDefault.
        if (
            this._isScrolling ||
            document.visibilityState === "hidden" ||
            this.overlayManager.active
        ) {
            return;
        }

        if (isPinchToZoom && supportsPinchToZoom) {
            scaleFactor = this._accumulateFactor(
                pdfViewer.currentScale,
                scaleFactor,
                "_wheelUnusedFactor"
            );
            this.updateZoom(null, scaleFactor, origin);
        } else {
            const delta = normalizeWheelEventDirection(evt);

            let ticks = 0;
            if (
                deltaMode === WheelEvent.DOM_DELTA_LINE ||
                deltaMode === WheelEvent.DOM_DELTA_PAGE
            ) {
                // For line-based devices, use one tick per event, because different
                // OSs have different defaults for the number lines. But we generally
                // want one "clicky" roll of the wheel (which produces one event) to
                // adjust the zoom by one step.
                //
                // If we're getting fractional lines (I can't think of a scenario
                // this might actually happen), be safe and use the accumulator.
                ticks =
                    Math.abs(delta) >= 1
                        ? Math.sign(delta)
                        : this._accumulateTicks(delta, "_wheelUnusedTicks");
            } else {
                // pixel-based devices
                const PIXELS_PER_LINE_SCALE = 30;
                ticks = this._accumulateTicks(
                    delta / PIXELS_PER_LINE_SCALE,
                    "_wheelUnusedTicks"
                );
            }

            this.updateZoom(ticks, null, origin);
        }
    }
}

function onTouchStart(evt) {
    if (this.pdfViewer.isInPresentationMode || evt.touches.length < 2) {
        return;
    }
    evt.preventDefault();

    if (evt.touches.length !== 2 || this.overlayManager.active) {
        this._touchInfo = null;
        return;
    }

    let [touch0, touch1] = evt.touches;
    if (touch0.identifier > touch1.identifier) {
        [touch0, touch1] = [touch1, touch0];
    }
    this._touchInfo = {
        touch0X: touch0.pageX,
        touch0Y: touch0.pageY,
        touch1X: touch1.pageX,
        touch1Y: touch1.pageY,
    };
}

function onTouchMove(evt) {
    if (!this._touchInfo || evt.touches.length !== 2) {
        return;
    }

    const {pdfViewer, _touchInfo, supportsPinchToZoom} = this;
    let [touch0, touch1] = evt.touches;
    if (touch0.identifier > touch1.identifier) {
        [touch0, touch1] = [touch1, touch0];
    }
    const {pageX: page0X, pageY: page0Y} = touch0;
    const {pageX: page1X, pageY: page1Y} = touch1;
    const {
        touch0X: pTouch0X,
        touch0Y: pTouch0Y,
        touch1X: pTouch1X,
        touch1Y: pTouch1Y,
    } = _touchInfo;

    if (
        Math.abs(pTouch0X - page0X) <= 1 &&
        Math.abs(pTouch0Y - page0Y) <= 1 &&
        Math.abs(pTouch1X - page1X) <= 1 &&
        Math.abs(pTouch1Y - page1Y) <= 1
    ) {
        // Touches are really too close and it's hard do some basic
        // geometry in order to guess something.
        return;
    }

    _touchInfo.touch0X = page0X;
    _touchInfo.touch0Y = page0Y;
    _touchInfo.touch1X = page1X;
    _touchInfo.touch1Y = page1Y;

    if (pTouch0X === page0X && pTouch0Y === page0Y) {
        // First touch is fixed, if the vectors are collinear then we've a pinch.
        const v1X = pTouch1X - page0X;
        const v1Y = pTouch1Y - page0Y;
        const v2X = page1X - page0X;
        const v2Y = page1Y - page0Y;
        const det = v1X * v2Y - v1Y * v2X;
        // 0.02 is approximatively sin(0.15deg).
        if (Math.abs(det) > 0.02 * Math.hypot(v1X, v1Y) * Math.hypot(v2X, v2Y)) {
            return;
        }
    } else if (pTouch1X === page1X && pTouch1Y === page1Y) {
        // Second touch is fixed, if the vectors are collinear then we've a pinch.
        const v1X = pTouch0X - page1X;
        const v1Y = pTouch0Y - page1Y;
        const v2X = page0X - page1X;
        const v2Y = page0Y - page1Y;
        const det = v1X * v2Y - v1Y * v2X;
        if (Math.abs(det) > 0.02 * Math.hypot(v1X, v1Y) * Math.hypot(v2X, v2Y)) {
            return;
        }
    } else {
        const diff0X = page0X - pTouch0X;
        const diff1X = page1X - pTouch1X;
        const diff0Y = page0Y - pTouch0Y;
        const diff1Y = page1Y - pTouch1Y;
        const dotProduct = diff0X * diff1X + diff0Y * diff1Y;
        if (dotProduct >= 0) {
            // The two touches go in almost the same direction.
            return;
        }
    }

    evt.preventDefault();

    const origin = [(page0X + page1X) / 2, (page0Y + page1Y) / 2];
    const distance = Math.hypot(page0X - page1X, page0Y - page1Y) || 1;
    const pDistance = Math.hypot(pTouch0X - pTouch1X, pTouch0Y - pTouch1Y) || 1;
    if (supportsPinchToZoom) {
        const newScaleFactor = this._accumulateFactor(
            pdfViewer.currentScale,
            distance / pDistance,
            "_touchUnusedFactor"
        );
        this.updateZoom(null, newScaleFactor, origin);
    } else {
        const PIXELS_PER_LINE_SCALE = 30;
        const ticks = this._accumulateTicks(
            (distance - pDistance) / PIXELS_PER_LINE_SCALE,
            "_touchUnusedTicks"
        );
        this.updateZoom(ticks, null, origin);
    }
}

function onTouchEnd(evt) {
    if (!this._touchInfo) {
        return;
    }

    evt.preventDefault();
    this._touchInfo = null;
    this._touchUnusedTicks = 0;
    this._touchUnusedFactor = 1;
}

function onClick(evt) {
    // 点击后证快捷键可正常使用，select 等也可正常使用 https://github.com/siyuan-note/siyuan/issues/7869
    if (!["SELECT", "TEXTAREA", "INPUT"].includes(evt.target.tagName)) {
        this.pdfViewer.focus();
    }

    if (!this.secondaryToolbar?.isOpen) {
        return;
    }
    const appConfig = this.appConfig;
    if (
        this.pdfViewer.containsElement(evt.target) ||
        (appConfig.toolbar?.container.contains(evt.target) &&
            // TODO: change the `contains` for an equality check when the bug:
            //  https://bugzilla.mozilla.org/show_bug.cgi?id=1921984
            // is fixed.
            !appConfig.secondaryToolbar?.toggleButton.contains(evt.target))
    ) {
        this.secondaryToolbar.close();
    }
}

function onKeyUp(evt) {
    // NOTE
    const pdfInstance = getPdfInstance(evt.target)
    if (!pdfInstance) {
        return
    }
    // NOTE 4+ 版本不知道为 r 后不 focus 了
    if (!["SELECT", "TEXTAREA", "INPUT"].includes(evt.target.tagName)) {
        pdfInstance.pdfViewer.focus();
    }

    // evt.ctrlKey is false hence we use evt.key.
    if (evt.key === "Control") {
        pdfInstance._isCtrlKeyDown = false;
    }
    // NOTE 快捷键高亮取消
    if (([92, 91, 68].includes(evt.keyCode) || evt.ctrlKey || evt.altKey) && pdfInstance.appConfig.toolbar.rectAnno.classList.contains('toggled')) {
        pdfInstance.appConfig.toolbar.rectAnno.dispatchEvent(new MouseEvent('click'))
    }
}

function onKeyDown(evt) {
    // NOTE
    const pdfInstance = getPdfInstance(evt.target)
    if (!pdfInstance) {
        return
    }
    pdfInstance._isCtrlKeyDown = evt.key === "Control";

    if (pdfInstance.overlayManager.active) {
        return;
    }
    const {eventBus, pdfViewer} = pdfInstance;
    const isViewerInPresentationMode = pdfViewer.isInPresentationMode;

    let handled = false,
        ensureViewerFocused = false;
    const cmd =
        (evt.ctrlKey ? 1 : 0) |
        (evt.altKey ? 2 : 0) |
        (evt.shiftKey ? 4 : 0) |
        (evt.metaKey ? 8 : 0);
    if (cmd === 0 && [38, 40].includes(evt.keyCode)) {
        // NOTE https://github.com/siyuan-note/siyuan/issues/8164
        if (document.activeElement) {
            document.activeElement.blur();
        }
        setTimeout(() => {
            pdfViewer.focus();
        })
        return;
    }

    // NOTE
    if (!evt.repeat && (cmd === 8 || cmd === 1 || cmd === 2) && evt.keyCode === 68 &&  // D
        !pdfInstance.appConfig.toolbar.rectAnno.classList.contains('toggled')) {
        pdfInstance.appConfig.toolbar.rectAnno.dispatchEvent(
            new MouseEvent('click'))
        evt.preventDefault()
        return
    }

    if (!evt.repeat && cmd !== 1 && cmd !== 2 && cmd !== 4 && cmd !== 8 &&
        [48, 49, 50, 51, 52, 53, 54, 55].includes(evt.keyCode) &&
        getSelection().rangeCount > 0 &&
        !pdfInstance.appConfig.toolbar.rectAnno.classList.contains('toggled')) {
        const range = getSelection().getRangeAt(0);
        if (range.toString() !== "" && hasClosestByClassName(range.commonAncestorContainer, "pdfViewer")) {
            pdfInstance.appConfig.appContainer.dispatchEvent(new CustomEvent("click", {detail: (evt.keyCode - 48).toString()}));
            evt.preventDefault()
            return
        }
    }

    // First, handle the key bindings that are independent whether an input
    // control is selected or not.
    if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) {
        // either CTRL or META key with optional SHIFT.
        switch (evt.keyCode) {
            case 70: // f
                if (!pdfInstance.supportsIntegratedFind && !evt.shiftKey) {
                    pdfInstance.findBar?.open();
                    handled = true;
                }
                break;
            case 71: // g
                if (!pdfInstance.supportsIntegratedFind) {
                    const {state} = pdfInstance.findController;
                    if (state) {
                        const newState = {
                            source: window,
                            type: "again",
                            findPrevious: cmd === 5 || cmd === 12,
                        };
                        eventBus.dispatch("find", {...state, ...newState});
                    }
                    handled = true;
                }
                break;
            case 61: // FF/Mac '='
            case 107: // FF '+' and '='
            case 187: // Chrome '+'
            case 171: // FF with German keyboard
                pdfInstance.zoomIn();
                handled = true;
                break;
            case 173: // FF/Mac '-'
            case 109: // FF '-'
            case 189: // Chrome '-'
                pdfInstance.zoomOut();
                handled = true;
                break;
            case 48: // '0'
            case 96: // '0' on Numpad of Swedish keyboard
                if (!isViewerInPresentationMode) {
                    // keeping it unhandled (to restore page zoom to 100%)
                    setTimeout(() => {
                        // ... and resetting the scale after browser adjusts its scale
                        pdfInstance.zoomReset();
                    });
                    handled = false;
                }
                break;

            case 38: // up arrow
                if (isViewerInPresentationMode || pdfInstance.page > 1) {
                    pdfInstance.page = 1;
                    handled = true;
                    ensureViewerFocused = true;
                }
                break;
            case 40: // down arrow
                if (isViewerInPresentationMode || pdfInstance.page < pdfInstance.pagesCount) {
                    pdfInstance.page = pdfInstance.pagesCount;
                    handled = true;
                    ensureViewerFocused = true;
                }
                break;
        }
    }

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC || CHROME")) {
        // CTRL or META without shift
        if (cmd === 1 || cmd === 8) {
            switch (evt.keyCode) {
                case 83: // s
                    eventBus.dispatch("download", {source: window});
                    handled = true;
                    break;

                case 79: // o
                    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
                        eventBus.dispatch("openfile", {source: window});
                        handled = true;
                    }
                    break;
            }
        }
    }

    // CTRL+ALT or Option+Command
    if (cmd === 3 || cmd === 10) {
        switch (evt.keyCode) {
            case 80: // p
                pdfInstance.requestPresentationMode();
                handled = true;
                pdfInstance.externalServices.reportTelemetry({
                    type: "buttons",
                    data: {id: "presentationModeKeyboard"},
                });
                break;
            case 71: // g
                // focuses input#pageNumber field
                if (pdfInstance.appConfig.toolbar) {
                    pdfInstance.appConfig.toolbar.pageNumber.select();
                    handled = true;
                }
                break;
        }
    }

    if (handled) {
        if (ensureViewerFocused && !isViewerInPresentationMode) {
            pdfViewer.focus();
        }
        evt.preventDefault();
        return;
    }

    // Some shortcuts should not get handled if a control/input element
    // is selected.
    const curElement = getActiveOrFocusedElement();
    const curElementTagName = curElement?.tagName.toUpperCase();
    if (
        curElementTagName === "INPUT" ||
        curElementTagName === "TEXTAREA" ||
        curElementTagName === "SELECT" ||
        (curElementTagName === "BUTTON" &&
            (evt.keyCode === /* Enter = */ 13 || evt.keyCode === /* Space = */ 32)) ||
        curElement?.isContentEditable
    ) {
        // Make sure that the secondary toolbar is closed when Escape is pressed.
        if (evt.keyCode !== /* Esc = */ 27) {
            return;
        }
    }

    // No control key pressed at all.
    if (cmd === 0) {
        let turnPage = 0,
            turnOnlyIfPageFit = false;
        switch (evt.keyCode) {
            case 38: // up arrow
                if (pdfInstance.supportsCaretBrowsingMode) {
                    pdfInstance.moveCaret(/* isUp = */ true, /* select = */ false);
                    handled = true;
                    break;
                }
            /* falls through */
            case 33: // pg up
                // vertical scrolling using arrow/pg keys
                if (pdfViewer.isVerticalScrollbarEnabled) {
                    turnOnlyIfPageFit = true;
                }
                turnPage = -1;
                break;
            case 8: // backspace
                if (!isViewerInPresentationMode) {
                    turnOnlyIfPageFit = true;
                }
                turnPage = -1;
                break;
            case 37: // left arrow
                if (pdfInstance.supportsCaretBrowsingMode) {
                    return;
                }
                // horizontal scrolling using arrow keys
                if (pdfViewer.isHorizontalScrollbarEnabled) {
                    turnOnlyIfPageFit = true;
                }
            /* falls through */
            case 75: // 'k'
            case 80: // 'p'
                turnPage = -1;
                break;
            case 27: // esc key
                if (pdfInstance.secondaryToolbar?.isOpen) {
                    pdfInstance.secondaryToolbar.close();
                    handled = true;
                }
                if (!pdfInstance.supportsIntegratedFind && pdfInstance.findBar?.opened) {
                    pdfInstance.findBar.close();
                    handled = true;
                }
                break;
            case 40: // down arrow
                if (pdfInstance.supportsCaretBrowsingMode) {
                    pdfInstance.moveCaret(/* isUp = */ false, /* select = */ false);
                    handled = true;
                    break;
                }
            /* falls through */
            case 34: // pg down
                // vertical scrolling using arrow/pg keys
                if (pdfViewer.isVerticalScrollbarEnabled) {
                    turnOnlyIfPageFit = true;
                }
                turnPage = 1;
                break;
            case 13: // enter key
            case 32: // spacebar
                if (!isViewerInPresentationMode) {
                    turnOnlyIfPageFit = true;
                }
                turnPage = 1;
                break;
            case 39: // right arrow
                if (pdfInstance.supportsCaretBrowsingMode) {
                    return;
                }
                // horizontal scrolling using arrow keys
                if (pdfViewer.isHorizontalScrollbarEnabled) {
                    turnOnlyIfPageFit = true;
                }
            /* falls through */
            case 74: // 'j'
            case 78: // 'n'
                turnPage = 1;
                break;

            case 36: // home
                if (isViewerInPresentationMode || pdfInstance.page > 1) {
                    pdfInstance.page = 1;
                    handled = true;
                    ensureViewerFocused = true;
                }
                break;
            case 35: // end
                if (isViewerInPresentationMode || pdfInstance.page < pdfInstance.pagesCount) {
                    pdfInstance.page = pdfInstance.pagesCount;
                    handled = true;
                    ensureViewerFocused = true;
                }
                break;

            case 83: // 's'
                pdfInstance.pdfCursorTools?.switchTool(CursorTool.SELECT);
                break;
            case 72: // 'h'
                pdfInstance.pdfCursorTools?.switchTool(CursorTool.HAND);
                break;

            case 82: // 'r'
                pdfInstance.rotatePages(90);
                break;

            case 115: // F4
                pdfInstance.pdfSidebar?.toggle();
                break;
        }

        if (
            turnPage !== 0 &&
            (!turnOnlyIfPageFit || pdfViewer.currentScaleValue === "page-fit")
        ) {
            if (turnPage > 0) {
                pdfViewer.nextPage();
            } else {
                pdfViewer.previousPage();
            }
            handled = true;
        }
    }

    // shift-key
    if (cmd === 4) {
        switch (evt.keyCode) {
            case 13: // enter key
            case 32: // spacebar
                if (
                    !isViewerInPresentationMode &&
                    pdfViewer.currentScaleValue !== "page-fit"
                ) {
                    break;
                }
                pdfViewer.previousPage();

                handled = true;
                break;

            case 38: // up arrow
                pdfInstance.moveCaret(/* isUp = */ true, /* select = */ true);
                handled = true;
                break;
            case 40: // down arrow
                pdfInstance.moveCaret(/* isUp = */ false, /* select = */ true);
                handled = true;
                break;
            case 82: // 'r'
                pdfInstance.rotatePages(-90);
                break;
        }
    }

    if (!handled && !isViewerInPresentationMode) {
        // 33=Page Up  34=Page Down  35=End    36=Home
        // 37=Left     38=Up         39=Right  40=Down
        // 32=Spacebar
        if (
            (evt.keyCode >= 33 && evt.keyCode <= 40) ||
            (evt.keyCode === 32 && curElementTagName !== "BUTTON")
        ) {
            ensureViewerFocused = true;
        }
    }

    if (ensureViewerFocused && !pdfViewer.containsElement(curElement)) {
        // The page container is not focused, but a page navigation key has been
        // pressed. Change the focus to the viewer container to make sure that
        // navigation by keyboard works as expected.
        pdfViewer.focus();
    }

    if (handled) {
        evt.preventDefault();
    }
}

function beforeUnload(evt) {
    evt.preventDefault();
    evt.returnValue = "";
    return false;
}

// NOTE
export {
    PDFViewerApplication,
    onPageNumberChanged
};
